﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace TanmiaGrp.Basis.Core
{
    /// <summary>
    /// Represent the base class for BusinessEntity in a Business Application.
    /// </summary>
    public class BusinessObject : NotificationObject, IDataErrorInfo, ICloneable, IValidatableObject
    {
        private PropertyInfo[] props;
        private DateTime createDate;
        private DateTime? lastUpdateDate;

        /// <summary>
        /// Determines if object is newly created, or populated by Repository.
        /// </summary>
        /// <remarks>
        /// Notes for Inheritors:
        /// 1- Return true when the overridden object has an Id, False otherwise.
        /// 2- This value is used to determine if the can be deleted or not.
        /// </remarks>
        [NotMapped]
        public virtual bool IsEmpty
        {
            get
            {
                Type bizObjType = GetType();

                string typeName = bizObjType.Name;
                if (typeName.Contains('_'))
                    typeName = bizObjType.BaseType.Name;

                string primaryPropertyName = string.Format("{0}id", typeName.ToLower());
                PropertyInfo primaryProperty = props.FirstOrDefault(x => primaryPropertyName.Equals(x.Name.ToLower()));
                int value = Convert.ToInt32(primaryProperty.GetValue(this));

                return value == 0;
            }
        }

        public BusinessObject()
        {
            // get information of all properties in this class, except the read-only, static,
            // and virtual properties.
            props = GetType()
                    .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                    .Where(x => x.CanRead && x.CanWrite && !x.GetGetMethod().IsVirtual & !x.GetSetMethod().IsVirtual)
                    .ToArray();

            // because null and string.Empty is different and that will effect the GetHashCode value
            // I initiate all string properties with string.Empty as default values.
            InitiateDefaultValues();
        }

        #region Capture State of the BusinessObject

        private int capturedHashCode;

        /// <summary>
        /// Make and keep the current state of the object, to be compared later.
        /// </summary>
        public void CaptureState()
        {
            InitiateDefaultValues();
            capturedHashCode = GetHashCode();
        }

        /// <summary>
        /// Determines if current state is changed from last Capture operation.
        /// </summary>
        [NotMapped]
        public bool IsStateChanged
        {
            get
            {
                int tmpHashCode = GetHashCode();
                return tmpHashCode != capturedHashCode;
            }
        }

        #endregion

        #region Overridden Members

        /// <inheritdoc />
        public override bool Equals(object obj)
        {
            if (obj == null) return false;

            if (GetType() != obj.GetType()) return false;

            // use this pattern to compare reference members
            //if (!Object.Equals(Name, cust.Name)) return false;

            // use this pattern to compare value members
            //if (!Age.Equals(cust.Age)) return false;

            return true;
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            // start with the hash-code of the object.
            int hashCode = 0;//base.GetHashCode();

            foreach (PropertyInfo prop in props)
            {
                object value = prop.GetValue(this);

                if (prop.PropertyType == typeof(Boolean))
                    hashCode += Convert.ToBoolean(value).GetHashCode();
                else if (prop.PropertyType == typeof(Byte))
                    hashCode += Convert.ToByte(value).GetHashCode();
                else if (prop.PropertyType == typeof(Char))
                    hashCode += Convert.ToChar(value).GetHashCode();
                else if (prop.PropertyType == typeof(DateTime))
                    hashCode += Convert.ToDateTime(value).GetHashCode();
                else if (prop.PropertyType == typeof(Decimal))
                    hashCode += Convert.ToDecimal(value).GetHashCode();
                else if (prop.PropertyType == typeof(Double))
                    hashCode += Convert.ToDouble(value).GetHashCode();
                else if (prop.PropertyType == typeof(Int16))
                    hashCode += Convert.ToInt16(value).GetHashCode();
                else if (prop.PropertyType == typeof(Int32))
                    hashCode += Convert.ToInt32(value).GetHashCode();
                else if (prop.PropertyType == typeof(Int64))
                    hashCode += Convert.ToInt64(value).GetHashCode();
                else if (prop.PropertyType == typeof(SByte))
                    hashCode += Convert.ToSByte(value).GetHashCode();
                else if (prop.PropertyType == typeof(Single))
                    hashCode += Convert.ToSingle(value).GetHashCode();
                else if (prop.PropertyType == typeof(String))
                    hashCode += Convert.ToString(value).GetHashCode();
                else if (prop.PropertyType == typeof(UInt16))
                    hashCode += Convert.ToUInt16(value).GetHashCode();
                else if (prop.PropertyType == typeof(UInt32))
                    hashCode += Convert.ToUInt32(value).GetHashCode();
                else if (prop.PropertyType == typeof(UInt64))
                    hashCode += Convert.ToUInt64(value).GetHashCode();
                else if (value != null && typeof(BusinessObject).IsAssignableFrom(prop.PropertyType))
                    hashCode += value.GetHashCode();
                else if (prop.PropertyType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(BusinessObjectCollection<>)))
                    if (prop.PropertyType.GetGenericArguments().Any(x => typeof(BusinessObject).IsAssignableFrom(x)))
                        if (value != null)
                            foreach (BusinessObject bizObj in value as IEnumerable)
                                hashCode += bizObj.GetHashCode();
            }

            return hashCode;
        }

        #endregion

        /// <summary>
        /// Initiates the properties of the object with the default values
        /// 1. string -> string.Empty
        /// </summary>
        private void InitiateDefaultValues()
        {
            foreach (PropertyInfo prop in props)
            {
                if (prop.PropertyType == typeof(string))
                {
                    if (prop.GetValue(this) == null)
                        prop.SetValue(this, string.Empty);
                }
                else if (prop.PropertyType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>)))
                    if (prop.PropertyType.GetGenericArguments().Any(x => typeof(BusinessObject).IsAssignableFrom(x)))
                        if (prop.GetValue(this) == null)
                        {
                            Type collectionType = typeof(ObservableCollection<>);

                            prop.SetValue(this, Activator.CreateInstance(prop.PropertyType));
                        }
            }
        }

        #region IDataErrorInfo Members

        private bool isValid;

        private ObservableCollection<ValidationResult> validationResults;

        /// <summary>
        /// Determines if current business object has no validation issue or error.
        /// </summary>
        [NotMapped]
        public bool IsValid
        {
            get { return isValid; }
            private set
            {
                if (isValid != value)
                {
                    isValid = value;
                    RaisePropertyChanged(() => IsValid);
                }
            }
        }

        /// <summary>
        /// Represents all the validation result of current business object.
        /// This property is influenced by Validate method call.
        /// </summary>
        [NotMapped]
        public ObservableCollection<ValidationResult> ValidationResults
        {
            get { return validationResults; }
            private set
            {
                if (validationResults != value)
                {
                    this.validationResults = value;
                    RaisePropertyChanged(() => ValidationResults);
                }
            }
        }

        /// <summary>
        /// Validates all properties of current business object.
        /// This method influence IsValid, and ValidationResults properties.
        /// </summary>
        public void Validate()
        {
            ValidationResults = new ObservableCollection<ValidationResult>();

            System.ComponentModel.DataAnnotations.Validator.TryValidateObject(this, new ValidationContext(this, null, null), ValidationResults, true);

            IsValid = ValidationResults.Count == 0;
        }

        /// <summary>
        /// Gets an error message indicating what is wrong with this object.
        /// Invoking this properties will influence IsValid, and ValidationResults properties.
        /// </summary>
        [NotMapped]
        public string Error
        {
            get
            {
                Validate();

                var result = from x in ValidationResults
                             from y in x.ErrorMessage
                             select y;

                return string.Join(Environment.NewLine, result);
            }
        }

        /// <summary>
        /// Gets the error message for the property with the given name.
        /// Invoking this indexer will influence IsValid, and ValidationResults properties.
        /// </summary>
        /// <param name="propertyName">The name of the property whose error message to get.</param>
        /// <returns>The error message for the property. The default is an empty string ("").</returns>
        public string this[string propertyName]
        {
            get
            {
                Validate();

                ValidationResult result = (from x in ValidationResults
                                           from y in x.MemberNames
                                           where y == propertyName
                                           select x).FirstOrDefault();

                return (result != null) ? result.ErrorMessage : string.Empty;
            }
        }

        #endregion

        #region ICloneable Members

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>A new object that is a copy of this instance.</returns>
        public virtual object Clone()
        {
            object newObject = Activator.CreateInstance(GetType());

            foreach (PropertyInfo prop in props)
            {
                object newValue = null;
                object oldValue = prop.GetValue(this);

                if (oldValue != null && typeof(ICloneable).IsAssignableFrom(prop.PropertyType))
                {
                    newValue = ((ICloneable)prop.GetValue(this)).Clone();
                }
                else if (prop.PropertyType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>)))
                {
                    if (prop.PropertyType.GetGenericArguments().Any(x => typeof(BusinessObject).IsAssignableFrom(x)))
                    {
                        IList list = Activator.CreateInstance(prop.PropertyType) as IList;

                        foreach (ICloneable item in prop.GetValue(this) as IEnumerable)
                            list.Add(item.Clone());
                    }
                }
                else
                {
                    newValue = prop.GetValue(this);
                }

                prop.SetValue(newObject, newValue);
            }

            return newObject;
        }

        #endregion

        #region Common Members

        /// <summary>
        /// Gets or sets the creation date of current <see cref="BusinessObject"/>.
        /// </summary>
        [Display(Name = "تاريخ الإنشاء")]
        [Required]
        [Range(typeof(DateTime), "1/1/2014", "1/1/2050")]
        public virtual DateTime CreateDate
        {
            get { return createDate; }
            set
            {
                if (createDate != value)
                    if (createDate.Date != value.Date)
                    {
                        createDate = value;
                        RaisePropertyChanged(() => CreateDate);
                    }
            }
        }

        /// <summary>
        /// Gets or sets the last update date of current <see cref="BusinessObject"/>.
        /// </summary>
        [Display(Name = "تاريخ آخر تعديل")]
        [Range(typeof(DateTime), "1/1/2014", "1/1/2050")]
        public virtual DateTime? LastUpdateDate
        {
            get { return lastUpdateDate; }
            set
            {
                if (lastUpdateDate != value)
                {
                    lastUpdateDate = value;
                    RaisePropertyChanged(() => LastUpdateDate);
                }
            }
        }

        /// <summary>
        /// Gets or sets the row version of current <see cref="BusinessObject"/> that will used to check concurrency.
        /// </summary>
        [Timestamp]
        public byte[] RowVersion { get; set; }

        #endregion Common Members

        #region Presentation Members

        private bool isSelected;

        [NotMapped]
        public virtual bool IsSelected
        {
            get { return isSelected; }
            set
            {
                if (isSelected != value)
                {
                    isSelected = value;
                    RaisePropertyChanged(() => IsSelected);
                }
            }
        }
        #endregion

        #region IValidatableObject Members

        /// <summary>
        /// Determines whether the specified object is valid.
        /// </summary>
        /// <param name="validationContext"></param>
        /// <returns></returns>
        public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            yield break;
        }

        #endregion

        public void ClearTracking()
        {
            object value = null;
            MethodInfo clearTracking = null;

            foreach (PropertyInfo prop in props)
            {
                value = prop.GetValue(this);

                if (value != null)
                    clearTracking = value.GetType().GetMethod("ClearTracking");

                if (value != null && clearTracking != null)
                    clearTracking.Invoke(value, null);
            }
        }
    }
}
